Skip to content

Group-stepwise ORDC, load reserve provision, and OfflineReserve ORDC supply - #236

Open
rodrigomha wants to merge 18 commits into
mainfrom
rh/new_load_and_group_services
Open

Group-stepwise ORDC, load reserve provision, and OfflineReserve ORDC supply#236
rodrigomha wants to merge 18 commits into
mainfrom
rh/new_load_and_group_services

Conversation

@rodrigomha

@rodrigomha rodrigomha commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #235 (base branch rh/reserve_refactor); the diff shows only this feature's commits.

New features (not refactor items) re-expressed on the psy6 reserve tree:

  • GroupStepwiseCostReserve: elastic group ORDC. One dense ServiceRequirementVariable per group, a clearing constraint sum(member awards) >= requirement variable, and the group's demand curve (static or time series) priced through the existing delta-PWL path. Mis-paired ServiceModels (GroupReserve with a non-group formulation and vice versa) fail at declaration.
  • Load reserve provision under PowerLoadDispatch: the inverse of a generator. Up reserve is committed shed (P - r_up >= 0), down reserve is committed extra consumption (P + r_down <= forecast), gated on an attached service model. A costless load selling reserves errors since nothing pins its consumption.
  • OfflineReserve (non-spin) as ORDC supply from storage and loads: non-spinning is upward-only, so it routes like an up reserve everywhere a device supplies it (new UP_RESERVE union). Previously only thermal was wired: the storage reserve-balance multipliers MethodError'd, two storage coverage branches silently skipped the service, complete coverage hit an @assert false, and _modify_device_model! no-opped every OfflineReserve model. The no-op is now scoped to NonSpinningReserve, whose awards ride ReservePowerConstraint instead of the device range expressions.
  • Integration test: energy + reserve co-clearing with an elastic service, a stepwise group, and per-resource offers from thermal, storage, and load participants.

Tests live in existing files: group-stepwise testsets in test_services_constructor.jl; market integration, load provision, and the OfflineReserve ORDC scenarios in test_device_reserve_offers.jl; the storage non-spin coverage testset in test_storage_device_models.jl.

Full suite green: 106708/106708. Docs build green.

🤖 Generated with Claude Code

@rodrigomha

Copy link
Copy Markdown
Contributor Author

New commit 3479231: OfflineReserve can now supply an ORDC (StepwiseCostReserve) from storage and loads, not just thermal. Non-spin is upward-only, so it routes like an up reserve everywhere (new UP_RESERVE union). This fixes two silent skip branches and an @assert false in the storage coverage constraints, and scopes the _modify_device_model! no-op to NonSpinningReserve only. Full suite 106708/106708.

@rodrigomha rodrigomha changed the title Group-stepwise ORDC and load reserve provision Group-stepwise ORDC, load reserve provision, and OfflineReserve ORDC supply Aug 13, 2026
Base automatically changed from rh/reserve_refactor to main August 15, 2026 23:13
@jd-lara

jd-lara commented Aug 15, 2026

Copy link
Copy Markdown
Member

@rodrigomha this PR needs to be rebased

@rodrigomha

Copy link
Copy Markdown
Contributor Author

@jd-lara rebased onto main (post #235 squash-merge): the 7 feature commits replayed clean with zero conflicts (git rebase --onto main <235-head>), full suite green on the rebased branch (106708/106708). Base is already main.

@rodrigomha
rodrigomha force-pushed the rh/new_load_and_group_services branch from 3479231 to f33d289 Compare August 16, 2026 00:00
@rodrigomha

Copy link
Copy Markdown
Contributor Author

Done with the rebase

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Performance Results

Version Precompile Time
Main 4.398614896
This Branch 4.358017468
Version Build Time
Main-Build Time Precompile 67.694573733
Main-Build Time Postcompile 1.204987622
This Branch-Build Time Precompile 71.503647663
This Branch-Build Time Postcompile 1.099997209
Version Solve Time
Main-Solve Time Precompile 218.273362571
Main-Solve Time Postcompile 186.187219141
This Branch-Solve Time Precompile 106.977797223
This Branch-Solve Time Postcompile 73.839407253

@rodrigomha

Copy link
Copy Markdown
Contributor Author

CI status after the rebase, fully diagnosed:

  • Fixed on this branch (518ae0e, f0fda18): environment resolution. PSY psy6 now depends on the unregistered OpenAPI-models packages, and PSB psy6 on PowerTableDataParser/PowerFlowFileParser - Pkg ignores [sources] of non-root projects, so POM's root/test/docs environments now pin them directly. Docs, build, and comparison jobs are green.
  • Remaining test-job failures are upstream PSY psy6 bugs, reproduced and fixed locally (pending review before push): (1) the generated HydroPumpTurbine exporter calls _turbinepump_po, which was never added to the hand-written export helpers; (2) DOCUMENT_PLAN imports HydroPumpTurbine after HydroReservoir, which resolves its unit ids -> unresolved-id error; (3) FuelCurve.fuel_cost::TimeSeriesKey has no OpenAPI serde, which breaks PSB's bundle cache for c_sys5_re_fuel_cost (fixed via the schema's string variant + a document-level key-resolution pass).
  • With those PSY fixes applied locally: PSY test_openapi_document 448/448, POM full suite 106708/106708. No further POM commits needed - the pins float on psy6.
  • Separate flag: PSB's new bundle cache write is non-atomic; parallel first-builds can read a half-written system.json (UnexpectedEOF) - the old first-serialization race in a new spot. Worth a write-to-temp+rename in PSB.

…supply

One demand curve on a PSY.GroupReserve is cleared by the summed awards of its
contributing services: a dense ServiceRequirementVariable per group, a clearing
constraint sum(member awards) >= demand variable (its dual is the group price),
and the group curve priced through the delta-PWL path. Static and time-series
group curves are both supported via the existing service-side TS machinery.

- Group demand predicates mirror the service formulations: GroupRangeReserve is
  driven by the scalar requirement, GroupStepwiseCostReserve by the demand curve;
  degenerate groups skip as supply aggregates with a warning.
- Group deferral generalized to a vector so up and down groups coexist.
- RESERVE_PRODUCT_TYPES (definitions.jl) consolidates the open
  Union{PSY.AbstractReserve, PSY.GroupReserve} signature bound used across the
  reserve traits, PWL parameter chain, and objective plumbing.
- Formulation-pairing guards: a GroupReserve accepts only group formulations and
  vice versa, failing with ArgumentError at ServiceModel declaration.
- Tests cover build/solve, aggregation binding, merit order, no-group baseline,
  the degenerate skip, TS group curves, and the pairing guards.
PSY moved GroupReserve into the reserve tree, so the RESERVE_PRODUCT_TYPES
alias and the per-type methods that existed only because groups sat outside
it are gone: every former Union bound is plain PSY.AbstractReserve, the
group get_initial_conditions_service_model and the CostExpressions container
sibling fold into the AbstractReserve methods, and uses_compact_power opens
to the abstract type. Formulation-pair bounds
(Union{StepwiseCostReserve, GroupStepwiseCostReserve}) and the group demand
predicates stay - they encode formulation semantics, not typing.

The pairing guards merge with the #235 hardening set: valid direction-applied
pairs for both group formulations, the generic-defaults disambiguator, the
inverse guard over both formulations, and a direction-required error for
bare GroupReserve declarations.
A controllable load routes reserves inversely to generators: up reserve
is committed shed (P - r_up >= 0), down reserve is committed extra
consumption (P + r_down <= forecast). Dispatch limits move to the range
expressions only when a reserve service is attached, and a costless load
selling reserves fails loudly since nothing pins its consumption.
…-out

End-to-end market test: an elastic OnlineReserve (StepwiseCostReserve)
and a GroupStepwiseCostReserve group co-clear against per-resource
offers from thermal, storage, and load participants. Registers
GroupStepwiseCostReserve in the formulation library, refreshes the
stale group-reserve warnings there, and renames the remaining
market-specific reserve identifiers in hydro to generic ones.
Non-spinning is upward-only, so OfflineReserve routes like an up
reserve everywhere a device supplies it. New UP_RESERVE union in
reserve_traits.jl; storage reserve-balance multipliers, coverage
branches (two of which silently skipped OfflineReserve, one asserted),
get_fraction, and the TotalReserveOffering fold widened; load routing
and folding accept it as committed shed. The _modify_device_model!
no-op is scoped to NonSpinningReserve, whose awards ride
ReservePowerConstraint instead of the device range expressions.
PowerSystems psy6 (post schema-matching merge) depends on the
unregistered PowerCoreOpenAPIModels / PowerOperationsOpenAPIModels.
Pkg ignores [sources] of non-root projects, so each environment that
resolves PSY - root, test, docs - must pin them itself; CI failed with
'PowerOperationsOpenAPIModels has no known versions' on all jobs.
Pins mirror PSY's own (monorepo main, subdirs) and are temporary until
the packages are registered.
@rodrigomha
rodrigomha force-pushed the rh/new_load_and_group_services branch from f0fda18 to 3382f8f Compare August 17, 2026 17:05
@rodrigomha
rodrigomha changed the base branch from main to jd/network_matrix_consolidation August 17, 2026 17:05
@rodrigomha

Copy link
Copy Markdown
Contributor Author

Restacked on #233 per the network-consolidation sequencing: base is now jd/network_matrix_consolidation and the branch is rebased onto it (4e28ede), so the diff shows only this PR's commits.

  • The 7 feature commits replayed clean. Of the two CI environment-pin commits, the PSB transitive-deps one was dropped entirely and the OpenAPI one shrank to docs-env pins only - network matrix consolidation #233 already carries the same root/test pins (same non-root [sources] traps, fixed independently there).
  • Note network matrix consolidation #233 pins IOM at jd/network-sources; this stack inherits that.
  • Validation on the stacked branch with latest psy6 (which now includes the _turbinepump_po and hydro DOCUMENT_PLAN fixes): compile + zero method ambiguities, and the full suite 106951/106951. The one remaining upstream gap for CI is the FuelCurve.fuel_cost time-series serde in PSY (fix staged locally, pending review) - without it the thermal fuel-cost test fails via PSB's bundle cache.

Base automatically changed from jd/network_matrix_consolidation to main August 18, 2026 03:43
jd-lara and others added 7 commits August 17, 2026 21:44
…supply

One demand curve on a PSY.GroupReserve is cleared by the summed awards of its
contributing services: a dense ServiceRequirementVariable per group, a clearing
constraint sum(member awards) >= demand variable (its dual is the group price),
and the group curve priced through the delta-PWL path. Static and time-series
group curves are both supported via the existing service-side TS machinery.

- Group demand predicates mirror the service formulations: GroupRangeReserve is
  driven by the scalar requirement, GroupStepwiseCostReserve by the demand curve;
  degenerate groups skip as supply aggregates with a warning.
- Group deferral generalized to a vector so up and down groups coexist.
- RESERVE_PRODUCT_TYPES (definitions.jl) consolidates the open
  Union{PSY.AbstractReserve, PSY.GroupReserve} signature bound used across the
  reserve traits, PWL parameter chain, and objective plumbing.
- Formulation-pairing guards: a GroupReserve accepts only group formulations and
  vice versa, failing with ArgumentError at ServiceModel declaration.
- Tests cover build/solve, aggregation binding, merit order, no-group baseline,
  the degenerate skip, TS group curves, and the pairing guards.
PSY moved GroupReserve into the reserve tree, so the RESERVE_PRODUCT_TYPES
alias and the per-type methods that existed only because groups sat outside
it are gone: every former Union bound is plain PSY.AbstractReserve, the
group get_initial_conditions_service_model and the CostExpressions container
sibling fold into the AbstractReserve methods, and uses_compact_power opens
to the abstract type. Formulation-pair bounds
(Union{StepwiseCostReserve, GroupStepwiseCostReserve}) and the group demand
predicates stay - they encode formulation semantics, not typing.

The pairing guards merge with the #235 hardening set: valid direction-applied
pairs for both group formulations, the generic-defaults disambiguator, the
inverse guard over both formulations, and a direction-required error for
bare GroupReserve declarations.
A controllable load routes reserves inversely to generators: up reserve
is committed shed (P - r_up >= 0), down reserve is committed extra
consumption (P + r_down <= forecast). Dispatch limits move to the range
expressions only when a reserve service is attached, and a costless load
selling reserves fails loudly since nothing pins its consumption.
…-out

End-to-end market test: an elastic OnlineReserve (StepwiseCostReserve)
and a GroupStepwiseCostReserve group co-clear against per-resource
offers from thermal, storage, and load participants. Registers
GroupStepwiseCostReserve in the formulation library, refreshes the
stale group-reserve warnings there, and renames the remaining
market-specific reserve identifiers in hydro to generic ones.
rodrigomha and others added 3 commits August 18, 2026 09:56
Non-spinning is upward-only, so OfflineReserve routes like an up
reserve everywhere a device supplies it. New UP_RESERVE union in
reserve_traits.jl; storage reserve-balance multipliers, coverage
branches (two of which silently skipped OfflineReserve, one asserted),
get_fraction, and the TotalReserveOffering fold widened; load routing
and folding accept it as committed shed. The _modify_device_model!
no-op is scoped to NonSpinningReserve, whose awards ride
ReservePowerConstraint instead of the device range expressions.
PowerSystems psy6 (post schema-matching merge) depends on the
unregistered PowerCoreOpenAPIModels / PowerOperationsOpenAPIModels.
Pkg ignores [sources] of non-root projects, so each environment that
resolves PSY - root, test, docs - must pin them itself; CI failed with
'PowerOperationsOpenAPIModels has no known versions' on all jobs.
Pins mirror PSY's own (monorepo main, subdirs) and are temporary until
the packages are registered.
…na/PowerOperationsModels.jl into rh/new_load_and_group_services

@luke-kiernan luke-kiernan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine, no issues at a glance. However, you should probably have others review it too--I'm not very familiar the distinctions between these differennt types of reserves...

end

# Load up-reserve is committed shed: LB = P - Σ r_up, constrained >= 0. Generators route
# ReserveUp to the UB expression, so `V <: PSY.ElectricLoad` cannot shadow them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow the 2nd sentence of this comment

"""
Direction of a reserve. `OfflineReserve` (non-spinning) has no direction type parameter and is
upward-only in every US market, so it maps to [`PSY.ReserveUp`](@ref).
upward-only in every US market, so it maps to `PSY.ReserveUp`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lost (@ref). purposeful or AI?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants